from __future__ import with_statement

import os
import urllib2
import jsonlib
try:
    import cStringIO as StringIO
except ImportError:
    import StringIO
from contextlib import closing
from kineta.error import KinetaError
from kineta.filedb import Rollback


class RemoteDB(object):
    """
    Represents a connection to a database accessible via HTTP.

    The parameters and semantics of this class' methods are identical to the
    L{FileDB} class, with the exception of the L{__init__} method.

    Instances of this class are B{not} threadsafe.
    """

    def __init__(self, url):
        """
        Creates a remote connection object pointing to the specified URL

        @type url: string
        @param url: The URL at which the database is hosted.  This is generally
                    in the form of: http[s]://server/database_name.
        """
        self._pagelimit = 64 * 1024 * 1024
        if not url.endswith("/"):
            url = url + "/"
        self._url = url
        self.opener = urllib2.build_opener()
        """
        The urllib2 opener object which will be used to make HTTP requests.
        Users can modify or replace this variable to handle situations such as
        authentication or proxies.

        By default, contains the result of calling C{urllib2.build_opener()}.
        """
        self.headers = [('Content-Type', 'application/json; charset=utf-8')]
        """
        A list of extra HTTP headers to be sent with each request.  Each item
        is a tuple which is passed to the C{urllib2.Request.add_header} method.

        By default, contains
        C{[('Content-Type', 'application/json; charset=utf-8')]}.
        """

    def begin(self, isolated=False, txn=None):
        return _RemoteTxn(self,
                              self._docmd('begin', {'isolated': isolated,
                                                    'txn': self._txn_id(txn)}))

    def _commit(self, txnid):
        self._docmd('commit', {'txn': txnid})

    def _rollback(self, txnid):
        self._docmd('rollback', {'txn': txnid})

    def close(self):
        if 'opener' in self.__dict__:
            del self.opener

    def __enter__(self):
        return self

    def __exit__(self, *exc_info):
        self.close()

    def _docmd(self, name, d):
        req = urllib2.Request(self._url + name)
        for header in self.headers:
            req.add_header(*header)
        if isinstance(d, str):
            req.add_data(d)
        else:
            req.add_data(jsonlib.write(d, on_unknown=unicode))
        content = StringIO.StringIO()
        try:
            with closing(self.opener.open(req)) as resp:
                for line in resp:
                    content.write(line)
        except urllib2.HTTPError, e:
            line = e.readline()
            e.close()
            raise KinetaError("%s (%s)" % (str(e), line))
        ret = jsonlib.read(content.getvalue().decode('utf-8'))
        if u'next' not in ret:
            return ret[u'r']
        else:
            return ret[u'r'], ret[u'next']

    def _txn_id(self, txn):
        if txn is not None:
            return txn.id
        return None

    def _repeatcmd(self, name, static, array):
        retval = []

        prelude = jsonlib.write(static, on_unknown=unicode)[:-1] + ',"items":['

        stream = StringIO.StringIO()
        stream.write(prelude)
        for item in array:
            if stream.tell() > len(prelude):
                stream.write(',')

            jsonlib.dump(item, stream, on_unknown=unicode)

            if stream.tell() > self._pagelimit:
                stream.write(']}')
                retval.extend(self._docmd(name, stream.getvalue()))
                stream = StringIO.StringIO()
                stream.write(prelude)

        if stream.tell() > len(prelude):
            stream.write(']}')
            retval.extend(self._docmd(name, stream.getvalue()))

        return retval

    def insert(self, table, items, txn=None):
        if isinstance(items, dict):
            items = [items]
        return self._repeatcmd('insert', {'table': table,
                                          'txn': self._txn_id(txn)},
                                         items)

    def insert_bulk(self, table, columns, items, txn=None):
        return self._repeatcmd('insert_bulk', {'table': table,
                                               'columns': columns,
                                               'txn': self._txn_id(txn)},
                                              items)

    def select(self, table, fields, where=None, joins=None, order=None,
               limit=None, offset=None, txn=None):
        ret, next = self._docmd('select', {'table': table,
                                           'fields': fields,
                                           'where': where,
                                           'joins': joins,
                                           'order': order,
                                           'limit': limit,
                                           'offset': offset,
                                           'txn': self._txn_id(txn)})
        return _RemoteSelectReader(self, ret[0], ret[1], next)

    def explain(self, table, fields, where=None, joins=None, order=None,
                limit=None, offset=None, after=False, txn=None):
        plan = self._docmd('explain', {'table': table,
                                       'fields': fields,
                                       'where': where,
                                       'joins': joins,
                                       'order': order,
                                       'limit': limit,
                                       'offset': offset,
                                       'after': after,
                                       'txn': self._txn_id(txn)})
        return plan

    def update(self, table, values, where=None, limit=None, offset=None,
               txn=None):
        return self._docmd('update', {'table': table,
                                      'values': values,
                                      'where': where,
                                      'limit': limit,
                                      'offset': offset,
                                      'txn': self._txn_id(txn)})

    def delete(self, table, where=None, limit=None, offset=None, txn=None):
        return self._docmd('delete', {'table': table,
                                      'where': where,
                                      'limit': limit,
                                      'offset': offset,
                                      'txn': self._txn_id(txn)})

    def list_tables(self):
        return self._docmd('list_tables', {})

    def list_fields(self, table):
        return self._docmd('list_fields', {'table': table})

    def list_indexes(self, table):
        return self._docmd('list_indexes', {'table': table})

    def create_index(self, table, columns, permanent=False):
        return self._docmd('create_index', {'table': table,
                                            'columns': columns,
                                            'permanent': permanent})

    def drop_index(self, table, columns):
        return self._docmd('drop_index', {'table': table,
                                          'columns': columns})

    def drop_table(self, table):
        return self._docmd('drop_table', {'table': table})

    def wait_for_bg_tasks(self):
        return self._docmd('wait_for_bg_tasks', {})

_needs_txn = set(["begin", "insert", "insert_bulk",
                 "select", "explain", "update", "delete"])


class _RemoteTxn(object):

    def __init__(self, parent, id):
        self.id = id
        self._parent = parent
        self._isalive = True

    def commit(self):
        if self._isalive:
            self._parent._commit(self.id)
            self._isalive = False

    def rollback(self):
        if self._isalive:
            self._parent._rollback(self.id)
            self._isalive = False

    def __enter__(self):
        return self

    def __exit__(self, ex, val, tb):
        if tb is not None:
            self.rollback()
            return ex is Rollback
        else:
            self.commit()

    def __getattr__(self, name):
        attr = getattr(self.__dict__['_parent'], name)
        if name in _needs_txn:
            return lambda *args, **kwargs: attr(txn=self, *args, **kwargs)
        return attr


class _RemoteSelectReader(object):

    def __init__(self, db, names, items, next):
        self._db = db
        self.field_names = names
        self._items = items
        self._len = len(self._items)
        self._index = 0
        self._next = next
        self._asdict = False

    def __iter__(self):
        return self

    def as_dict(self):
        self._asdict = True
        return self

    def next(self):
        if self._index >= self._len:
            if self._next is None:
                raise StopIteration()
            else:
                (self._items, self._next) = \
                    self._db._docmd("select_next", {"next": self._next})
                self._index = 0
                self._len = len(self._items)
                return self.next()
        r = self._items[self._index]
        if self._asdict:
            d = {}
            for n, v in zip(self.field_names, r):
                d.setdefault(n[0], {})[n[1]] = v
            r = d
        self._index += 1
        return r

    def __enter__(self):
        return self

    def __exit__(self, *exc_info):
        self.close()

    def close(self):
        if self._next is not None:
            self._db._docmd("select_close", {"next": self._next})
            self.next = None
